|
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 || |