SVGHMI: many fixes on xy trend graph. WIP.
authorEdouard Tisserant
Wed, 11 May 2022 12:12:16 +0200
changeset 3484 32eaba9cf30e
parent 3474 3ba74350237d
child 3485 5f417d3c2d03
SVGHMI: many fixes on xy trend graph. WIP.
svghmi/Makefile
svghmi/gen_index_xhtml.ysl2
svghmi/pythonic.js
svghmi/widget_xygraph.ysl2
--- a/svghmi/Makefile	Fri May 06 11:04:54 2022 +0200
+++ b/svghmi/Makefile	Wed May 11 12:12:16 2022 +0200
@@ -15,7 +15,7 @@
 ysl2includes := $(filter-out $(ysl2files), $(wildcard *.ysl2))
 xsltfiles := $(patsubst %.ysl2, %.xslt, $(ysl2files))
 
-jsfiles := svghmi.js sprintf.js
+jsfiles := svghmi.js sprintf.js pythonic.js
 
 all:$(xsltfiles)
 
--- a/svghmi/gen_index_xhtml.ysl2	Fri May 06 11:04:54 2022 +0200
+++ b/svghmi/gen_index_xhtml.ysl2	Wed May 11 12:12:16 2022 +0200
@@ -96,6 +96,8 @@
 
                     include text sprintf.js
 
+                    include text pythonic.js
+
                     include text svghmi.js
 
                 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/pythonic.js	Wed May 11 12:12:16 2022 +0200
@@ -0,0 +1,206 @@
+/*
+
+From https://github.com/keyvan-m-sadeghi/pythonic
+
+Slightly modified in order to be usable in browser (i.e. not as a node.js module)
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Assister.Ai
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+class Iterator {
+    constructor(generator) {
+        this[Symbol.iterator] = generator;
+    }
+
+    async * [Symbol.asyncIterator]() {
+        for (const element of this) {
+            yield await element;
+        }
+    }
+
+    map(callback) {
+        const result = [];
+        for (const element of this) {
+            result.push(callback(element));
+        }
+
+        return result;
+    }
+
+    filter(callback) {
+        const result = [];
+        for (const element of this) {
+            if (callback(element)) {
+                result.push(element);
+            }
+        }
+
+        return result;
+    }
+
+    reduce(callback, initialValue) {
+        let empty = typeof initialValue === 'undefined';
+        let accumulator = initialValue;
+        let index = 0;
+        for (const currentValue of this) {
+            if (empty) {
+                accumulator = currentValue;
+                empty = false;
+                continue;
+            }
+
+            accumulator = callback(accumulator, currentValue, index, this);
+            index++;
+        }
+
+        if (empty) {
+            throw new TypeError('Reduce of empty Iterator with no initial value');
+        }
+
+        return accumulator;
+    }
+
+    some(callback) {
+        for (const element of this) {
+            if (callback(element)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    every(callback) {
+        for (const element of this) {
+            if (!callback(element)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    static fromIterable(iterable) {
+        return new Iterator(function * () {
+            for (const element of iterable) {
+                yield element;
+            }
+        });
+    }
+
+    toArray() {
+        return Array.from(this);
+    }
+
+    next() {
+        if (!this.currentInvokedGenerator) {
+            this.currentInvokedGenerator = this[Symbol.iterator]();
+        }
+
+        return this.currentInvokedGenerator.next();
+    }
+
+    reset() {
+        delete this.currentInvokedGenerator;
+    }
+}
+
+function rangeSimple(stop) {
+    return new Iterator(function * () {
+        for (let i = 0; i < stop; i++) {
+            yield i;
+        }
+    });
+}
+
+function rangeOverload(start, stop, step = 1) {
+    return new Iterator(function * () {
+        for (let i = start; i < stop; i += step) {
+            yield i;
+        }
+    });
+}
+
+function range(...args) {
+    if (args.length < 2) {
+        return rangeSimple(...args);
+    }
+
+    return rangeOverload(...args);
+}
+
+function enumerate(iterable) {
+    return new Iterator(function * () {
+        let index = 0;
+        for (const element of iterable) {
+            yield [index, element];
+            index++;
+        }
+    });
+}
+
+const _zip = longest => (...iterables) => {
+    if (iterables.length < 2) {
+        throw new TypeError("zip takes 2 iterables at least, "+iterables.length+" given");
+    }
+
+    return new Iterator(function * () {
+        const iterators = iterables.map(iterable => Iterator.fromIterable(iterable));
+        while (true) {
+            const row = iterators.map(iterator => iterator.next());
+            const check = longest ? row.every.bind(row) : row.some.bind(row);
+            if (check(next => next.done)) {
+                return;
+            }
+
+            yield row.map(next => next.value);
+        }
+    });
+};
+
+const zip = _zip(false), zipLongest= _zip(true);
+
+function items(obj) {
+    let {keys, get} = obj;
+    if (obj instanceof Map) {
+        keys = keys.bind(obj);
+        get = get.bind(obj);
+    } else {
+        keys = function () {
+            return Object.keys(obj);
+        };
+
+        get = function (key) {
+            return obj[key];
+        };
+    }
+
+    return new Iterator(function * () {
+        for (const key of keys()) {
+            yield [key, get(key)];
+        }
+    });
+}
+
+/*
+module.exports = {Iterator, range, enumerate, zip: _zip(false), zipLongest: _zip(true), items};
+*/
--- a/svghmi/widget_xygraph.ysl2	Fri May 06 11:04:54 2022 +0200
+++ b/svghmi/widget_xygraph.ysl2	Wed May 11 12:12:16 2022 +0200
@@ -40,7 +40,7 @@
             // not to clip data.
             this.clip = false;
 
-            let y_min = -Infinity, y_max = Infinity;
+            let y_min = Infinity, y_max = -Infinity;
 
             // Compute visible Y range by merging fixed curves Y ranges
             for(let minmax of this.minmaxes){
@@ -48,12 +48,12 @@
                    let [min,max] = minmax;
                    if(min < y_min)
                        y_min = min;
-                   if(max > y_max) 
+                   if(max > y_max)
                        y_max = max;
                }
             }
 
-            if(y_min !== -Infinity && y_max !== Infinity){
+            if(y_min !== Infinity && y_max !== -Infinity){
                this.fixed_y_range = true;
             } else {
                this.fixed_y_range = false;
@@ -66,16 +66,16 @@
             this.init_specific();
 
             this.reference = new ReferenceFrame(
-                [[this.x_interval_minor_mark, this.x_interval_major_mark],
-                 [this.y_interval_minor_mark, this.y_interval_major_mark]],
-                [this.y_axis_label, this.x_axis_label],
-                [this.x_axis_line, this.y_axis_line],
+                [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt],
+                 [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]],
+                [this.x_axis_label_elt, this.y_axis_label_elt],
+                [this.x_axis_line_elt, this.y_axis_line_elt],
                 [this.x_format, this.y_format]);
 
             // create <clipPath> path and attach it to widget
-            clipPath = document.createElementNS(xmlns,"clipPath");
-            clipPathPath = document.createElementNS(xmlns,"path");
-            clipPathPathDattr = document.createAttribute("d");
+            let clipPath = document.createElementNS(xmlns,"clipPath");
+            let clipPathPath = document.createElementNS(xmlns,"path");
+            let clipPathPathDattr = document.createAttribute("d");
             clipPathPathDattr.value = this.reference.getClipPathPathDattr();
             clipPathPath.setAttributeNode(clipPathPathDattr);
             clipPath.appendChild(clipPathPath);
@@ -86,7 +86,7 @@
                 curve.setAttribute("clip-path", "url(#" + clipPath.id + ")");
             }
 
-            this.curves_data = [];
+            this.curves_data = this.curves.map(_unused => []);
             this.max_data_length = this.args[0];
         }
 
@@ -119,6 +119,7 @@
                     this.ymin = value;
                 }
             }
+            let Ylength = this.ymax - this.ymin;
 
             // recompute X range based on curent time ad buffer depth
             // TODO: get PLC time instead of browser time
@@ -134,31 +135,30 @@
             // FIXME: use SVG getPathData and setPathData when available.
             //        https://svgwg.org/specs/paths/#InterfaceSVGPathData
             //        https://github.com/jarek-foksa/path-data-polyfill
-           
+
             let [base_point, xvect, yvect] = this.reference.getBaseRef();
-            this.curves_d_attr = 
-                zip(this.curves_data, this.curves).map(function([data,curve]){
-                let new_d = data.map(function([y, i]){
-                    // compute curve point from data, ranges, and base_ref
-                    let x = Xmin + i * Xlength / data_length;
-                    let xv = vectorscale(xvect, (x - Xmin) / Xlength);
-                    let yv = vectorscale(yvect, (y - Ymin) / Ylength);
-                    let px = base_point.x + xv.x + yv.x;
-                    let py = base_point.y + xv.y + yv.y;
-                    if(!this.fixed_y_range){
-                        if(ymin_damaged && y > this.ymin) this.ymin = y;
-                        if(xmin_damaged && x > this.xmin) this.xmin = x;
-                    }
-
-                    return " " + px + "," + py;
+            this.curves_d_attr =
+                zip(this.curves_data, this.curves).map(([data,curve]) => {
+                    let new_d = data.map((y, i) => {
+                        // compute curve point from data, ranges, and base_ref
+                        let x = this.xmin + i * Xlength / data_length;
+                        let xv = vectorscale(xvect, (x - this.xmin) / Xlength);
+                        let yv = vectorscale(yvect, (y - this.ymin) / Ylength);
+                        let px = base_point.x + xv.x + yv.x;
+                        let py = base_point.y + xv.y + yv.y;
+                        if(!this.fixed_y_range){
+                            if(ymin_damaged && y < this.ymin) this.ymin = y;
+                            if(ymax_damaged && y > this.ymax) this.ymax = y;
+                        }
+
+                        return " " + px + "," + py;
+                    });
+
+                    new_d.unshift("M ");
+
+                    return new_d.join('');
                 });
 
-                new_d.unshift("M ");
-                new_d.push(" z");
-
-                return new_d.join('');
-            }
-
             // computed curves "d" attr is applied to svg curve during animate();
 
             this.request_animate();
@@ -166,12 +166,17 @@
 
         animate(){
 
-            // move marks and update labels
-            this.reference.applyRanges([this.XRange, this.YRange]);
-
-            // apply computed curves "d" attributes
-            for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){
-                curve.setAttribute("d", d_attr);
+            // move elements only if enough data
+            if(this.curves_data.some(data => data.length > 1)){
+
+                // move marks and update labels
+                this.reference.applyRanges([[this.xmin, this.xmax],
+                                            [this.ymin, this.ymax]]);
+
+                // apply computed curves "d" attributes
+                for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){
+                    curve.setAttribute("d", d_attr);
+                }
             }
         }
 
@@ -179,8 +184,8 @@
 }
 
 widget_defs("XYGraph") {
-    labels("/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label")
-    labels("/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label")
+    labels("/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label");
+    labels("/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label");
 
     |     init_specific() {
 
@@ -219,12 +224,18 @@
 function move_elements_to_group(elements) {
     let newgroup = document.createElementNS(xmlns,"g");
     for(let element of elements){
-        element.parentElement().removeChild(element);
+        let parent = element.parentElement;
+        if(parent !== null)
+            parent.removeChild(element);
         newgroup.appendChild(element);
     }
     return newgroup;
 }
 function getLinesIntesection(l1, l2) {
+    let [l1start, l1vect] = l1;
+    let [l2start, l2vect] = l2;
+
+    let b;
     /*
     Compute intersection of two lines
     =================================
@@ -243,45 +254,35 @@
     intersection = l2start + l2vect * b
     ==> solve : "l1start + l1vect * a = l2start + l2vect * b" to find a and b and then intersection
 
-    (1)   l1start.x + l1vect.x * a = l2start.x + l2vect.x * b
-    (2)   l1start.y + l1vect.y * a = l2start.y + l2vect.y * b
+    (1)   l1start.y + l1vect.y * a = l2start.y + l2vect.y * b
+    (2)   l1start.x + l1vect.x * a = l2start.x + l2vect.x * b
 
     // express a
-    (1)   a = (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x)
+    (1)   a = (l2start.y + l2vect.y * b - l1start.y) / l1vect.y
 
     // substitute a to have only b
-    (1+2) l1start.y + l1vect.y * (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x) = l2start.y + l2vect.y * b
+    (1+2) l1start.x + l1vect.x * (l2start.y + l2vect.y * b - l1start.y) / l1vect.y = l2start.x + l2vect.x * b
 
     // expand to isolate b
-    (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
-    (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)
+    l1start.x + l1vect.x * l2start.y / l1vect.y + l2vect.y / l1vect.y * b - l1start.y / l1vect.y = l2start.x + l2vect.x * b
 
     // factorize b
-    (2) l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y = b * (l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x))
+    l1start.x + l1vect.x * l2start.y / l1vect.y - l1start.y / l1vect.y - l2start.x = b * ( l2vect.x - l2vect.y / l1vect.y)
 
     // extract b
-    (2) b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x)))
+    b = (l1start.x + l1vect.x * l2start.y / l1vect.y  - l1start.y / l1vect.y - l2start.x)  / ( l2vect.x - l2vect.y / l1vect.y)
     */
 
-    let [l1start, l1vect] = l1;
-    let [l2start, l2vect] = l2;
-
-    let b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x)));
+    /* avoid divison by zero by swapping (1) and (2) */
+    if(l1vect.y == 0 ){
+        b = (l1start.y + l1vect.y * l2start.x / l1vect.x  - l1start.x / l1vect.x - l2start.y)  / ( l2vect.y - l2vect.x / l1vect.x);
+    }else{
+        b = (l1start.x + l1vect.x * l2start.y / l1vect.y  - l1start.y / l1vect.y - l2start.x)  / ( l2vect.x - l2vect.y / l1vect.y);
+    }
 
     return new DOMPoint(l2start.x + l2vect.x * b, l2start.y + l2vect.y * b);
 };
 
-
-// From https://stackoverflow.com/a/48293566
-function *zip (...iterables){
-    let iterators = iterables.map(i => i[Symbol.iterator]() )
-    while (true) {
-        let results = iterators.map(iter => iter.next() )
-        if (results.some(res => res.done) ) return
-        else yield results.map(res => res.value )
-    }
-}
-
 class ReferenceFrame {
     constructor(
         // [[Xminor,Xmajor], [Yminor,Ymajor]]
@@ -293,7 +294,7 @@
         // [Xformat, Yformat] printf-like formating strings
         formats
     ){
-        this.axes = zip(labels,marks,lines,formats).map((...args) => new Axis(...args));
+        this.axes = zip(labels,marks,lines,formats).map(args => new Axis(...args));
 
         let [lx,ly] = this.axes.map(axis => axis.line);
         let [[xstart, xvect], [ystart, yvect]] = [lx,ly];
@@ -324,7 +325,7 @@
 
     applyRanges(ranges){
         for(let [range,axis] of zip(ranges,this.axes)){
-            axis.applyRange(range);
+            axis.applyRange(...range);
         }
     }
 
@@ -340,46 +341,19 @@
         Given axes lines are not starting at the same point, hereafter is
         calculus of parallelogram base point.
 
-                              ^ given Y axis
+                              ^ given Y axis (yvect)
                    /         /
                   /         /
                  /         /
-         xstart /---------/--------------> given X axis
-               /         /
+         xstart *---------*--------------> given X axis (xvect)
+               /         /origin
               /         /
-             /---------/--------------
+             *---------*--------------
         base_point   ystart
 
-        base_point = xstart + yvect * a
-        base_point = ystart + xvect * b
-        ==> solve : "xstart + yvect * a = ystart + xvect * b" to find a and b and then base_point
-
-        (1)   xstart.x + yvect.x * a = ystart.x + xvect.x * b
-        (2)   xstart.y + yvect.y * a = ystart.y + xvect.y * b
-
-        // express a
-        (1)   a = (ystart.x + xvect.x * b) / (xstart.x + yvect.x)
-
-        // substitute a to have only b
-        (1+2) xstart.y + yvect.y * (ystart.x + xvect.x * b) / (xstart.x + yvect.x) = ystart.y + xvect.y * b
-
-        // expand to isolate b
-        (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
-        (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)
-
-        // factorize b
-        (2) xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y = b * (xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x))
-
-        // extract b
-        (2) b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x)))
         */
 
-        let b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x)));
-        let base_point = new DOMPoint(ystart.x + xvect.x * b, ystart.y + xvect.y * b);
-
-        // // compute given origin
-        // // from drawing : given_origin = xstart - xvect * b
-        // let given_origin = new DOMPoint(xstart.x - xvect.x * b, xstart.y - xvect.y * b);
+        let base_point = getLinesIntesection([xstart,yvect],[ystart,xvect]);
 
         return base_point;
 
@@ -407,13 +381,13 @@
         };
 
         // group marks an labels together
-        let parent = line.parentElement()
-        marks_group = move_elements_to_group(marks);
-        marks_and_label_group = move_elements_to_group([marks_group_use, label]);
-        group = move_elements_to_group([marks_and_label_group,line]);
+        let parent = line.parentElement;
+        let marks_group = move_elements_to_group(marks);
+        let marks_and_label_group = move_elements_to_group([marks_group, label]);
+        let group = move_elements_to_group([marks_and_label_group,line]);
         parent.appendChild(group);
 
-        // Add transforms to group 
+        // Add transforms to group
         for(let name of ["base","origin"]){
             let transform = svg_root.createSVGTransform();
             group.transform.baseVal.appendItem(transform);
@@ -429,16 +403,16 @@
     }
 
     setBasePoint(base_point){
-        // move Axis to base point 
-        let [start, _vect] = this.lineElement;
+        // move Axis to base point
+        let [start, _vect] = this.line;
         let v = vector(start, base_point);
         this.base_transform.setTranslate(v.x, v.y);
 
-        // Move marks and label to base point. 
-        // _|_______         _|________
-        //  |  '  |     ==>   ' 
-        //  |     0           0
-        //  |                 |
+        // Move marks and label to base point.
+        // _|_______          _|________
+        //  |  '  |     ==>    '
+        //  |     0            0
+        //  |                  |
 
         for(let [markname,mark] of zip(["minor", "major"],this.marks)){
             let transform = this[markname+"_base_transform"];
@@ -456,31 +430,6 @@
         }
     }
 
-    applyOriginAndUnitVector(offset, unit_vect){
-        // offset is a representing position of an 
-        // axis along the opposit axis line, expressed in major marks units
-        // unit_vect is the translation in between to major marks
-
-        //              ^
-        //              | unit_vect
-        //              |<--->
-        //     _________|__________>
-        //     ^  |  '  |  '  |  '
-        //     |yoffset |     1 
-        //     |        |
-        //     v xoffset|
-        //     X<------>|
-        // base_point
-
-        // move major marks and label to first positive mark position
-        let v = vectorscale(unit_vect, offset+1);
-        this.label_slide_transform.setTranslate(v.x, v.x);
-        this.major_slide_transform.setTranslate(v.x, v.x);
-        // move minor mark to first half positive mark position
-        let h = vectorscale(unit_vect, offset+0.5);
-        this.minor_slide_transform.setTranslate(h.x, h.x);
-    }
-
     applyRange(min, max){
         let range = max - min;
 
@@ -544,7 +493,28 @@
         let mark_count = umax-umin;
 
         // apply unit vector to marks and label
-        this.label_and_marks.applyOriginAndUnitVector(offset, unit_vect);
+        // offset is a representing position of an 
+        // axis along the opposit axis line, expressed in major marks units
+        // unit_vect is the translation in between to major marks
+
+        //              ^
+        //              | unit_vect
+        //              |<--->
+        //     _________|__________>
+        //     ^  |  '  |  '  |  '
+        //     |yoffset |     1 
+        //     |        |
+        //     v xoffset|
+        //     X<------>|
+        // base_point
+
+        // move major marks and label to first positive mark position
+        let v = vectorscale(unit_vect, offset+unit);
+        this.label_slide_transform.setTranslate(v.x, v.y);
+        this.major_slide_transform.setTranslate(v.x, v.y);
+        // move minor mark to first half positive mark position
+        let h = vectorscale(unit_vect, offset+(unit/2));
+        this.minor_slide_transform.setTranslate(h.x, h.y);
 
         // duplicate marks and labels as needed
         let current_mark_count = this.mlg_clones.length;
@@ -552,15 +522,15 @@
             // cloneNode() label and add a svg:use of marks in a new group
             let newgroup = document.createElementNS(xmlns,"g");
             let transform = svg_root.createSVGTransform();
-            let newlabel = cloneNode(this.label);
+            let newlabel = this.label.cloneNode(true);
             let newuse = document.createElementNS(xmlns,"use");
             let newuseAttr = document.createAttribute("xlink:href");
             newuseAttr.value = "#"+this.marks_group.id;
-            newuse.setAttributeNode(newuseAttr.value);
+            newuse.setAttributeNode(newuseAttr);
             newgroup.transform.baseVal.appendItem(transform);
             newgroup.appendChild(newlabel);
             newgroup.appendChild(newuse);
-            this.mlg_clones.push([tranform,newgroup]);
+            this.mlg_clones.push([transform,newgroup]);
         }
 
         // move marks and labels, set labels
@@ -571,15 +541,15 @@
             let text = this.format ? sprintf(this.format, val) : val.toString();
             if(u == uoffset){
                 // apply offset to original marks and label groups
-                this.origin_transform.setTranslate(vec.x, vec.x);
+                this.origin_transform.setTranslate(vec.x, vec.y);
 
                 // update original label text
-                this.label_and_mark.label.textContent = text;
+                this.label.getElementsByTagName("tspan")[0].textContent = text;
             } else {
                 let [transform,element] = this.mlg_clones[i++];
 
                 // apply unit vector*N to marks and label groups
-                transform.setTranslate(vec.x, vec.x);
+                transform.setTranslate(vec.x, vec.y);
 
                 // update label text
                 element.getElementsByTagName("tspan")[0].textContent = text;